﻿using System;
using System.Xml;

using BizElements.BusinessLayer;

namespace BizElements.Web
{
    /// <summary>
    /// Defines how <see cref="ICommand"/> object is displayed in UI.
    /// </summary>
    public sealed class CommandStyle
    {
        #region DisableStyle enum.

        /// <summary>
        /// Specifies how a command which is not available to the current <see cref="IActor"/> is displayed.
        /// </summary>
        public enum DisableStyle
        {
            /// <summary>
            /// Indicates that a display component may automatically choose how to disable the command.
            /// </summary>
            Automatic,

            /// <summary>
            /// Indicates that a command should be displayed in the UI, but it cannot be executed.
            /// </summary>
            Disable,

            /// <summary>
            /// Indicates that a command should not be displayed if the actor does not have sufficient priviliges to execute it.
            /// </summary>
            Hide,

            /// <summary>
            /// Indicates that a command should be displayed and that the actor can invoke it's execution.
            /// </summary>
            None
        }

        #endregion

        #region FieldPermissions.

        string key;
        string text;
        string description;
        DisableStyle unavailableStyle = DisableStyle.Automatic;
        DisableStyle notGrantedStyle = DisableStyle.Automatic;
        ObjectProperties extendedProperties = new ObjectProperties();

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of <b>CommandStyle</b> class.
        /// </summary>
        public CommandStyle()
        { }

        /// <summary>
        /// Initializes a new instance of <b>CommandStyle</b> class.
        /// </summary>
        /// <param name="key">Command key. Null is not allowed.</param>
        /// <param name="text">Optional text. Nullable. Empty strings are converted to <b>null</b>.</param>
        /// <param name="description">Optional description. Nullable. Empty strings are converted to <b>null</b>.</param>
        /// <param name="unavailableStyle">Specifies how a command, which is not available to the current <see cref="IActor"/>, is displayed.</param>
        /// <param name="notGrantedStyle">Specfies how a command, which the current <see cref="IActor"/> is not granted to execute, is displayed.</param>
        /// <exception cref="ArgumentException">Generated if null reference or empty string is provided as a <b>Key</b> value.</exception>
        public CommandStyle(string key, string text, string description, DisableStyle unavailableStyle, DisableStyle notGrantedStyle)
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentException("key", "Null refence or empty string is not a valid 'Key' value.");

            this.key = key;
            this.text = text;
            this.description = description;
            this.unavailableStyle = unavailableStyle;
            this.notGrantedStyle = notGrantedStyle;
        }

        /// <summary>
        /// Initializes a new instance of <b>CommandStyle</b> class.
        /// </summary>
        /// <param name="command">ICommand instance whose Key and Text properties are copied to CommandStyle properties.</param>
        public CommandStyle(ICommand command)
        {
            if (command == null)
                throw new ArgumentNullException("command", "Null not allowed.");

            this.key = command.Key;
            this.text = command.Text;
        }

        #endregion

        #region FromXml.

        internal void FromXmlElement(XmlElement xmlCommandElement)
        {
            EnsureCorrectElement(xmlCommandElement, XML.Command);
            foreach (XmlAttribute attr in xmlCommandElement.Attributes)
                ParseCommandAttribute(attr);

            if (string.IsNullOrEmpty(this.text))
                throw new ArgumentException("xmlCommandElement", "'" + XML.Command + "' XML element does not provide a valid value for '" + XML.Key + "' attribute.");

            foreach (XmlNode child in xmlCommandElement.ChildNodes)
            {
                if (child.NodeType == XmlNodeType.Element)
                    ParseCommandChildElement((XmlElement)child);
            }
        }

        private static class XML
        {
            public const string Command = "Command";
            public const string Key = "Key";
            public const string Text = "Text";
            public const string Description = "Description";
            public const string UnavailableStyle = "UnavailableStyle";
            public const string NotGrantedStyle = "NotGrantedStyle";
            public const string ExtendedProperties = "ExtendedProperties";
            public const string Property = "Property";
            public const string Indexer = "Indexer";
            public const string Value = "Value";
            public const string Index = "Index";
        }

        private static void EnsureCorrectElement(XmlElement element, string expectedElementName)
        {
            if (element.LocalName != expectedElementName)
                throw new ArgumentException("Expected '" + expectedElementName + "' XML element but '" + element.LocalName + "'was provided.");
        }

        private void ParseCommandAttribute(XmlAttribute attr)
        {
            switch (attr.LocalName)
            {
                case XML.Key:
                    this.Key = attr.Value.Trim();
                    break;

                case XML.Text:
                    this.Text = attr.Value.Trim();
                    break;

                case XML.Description:
                    this.Description = attr.Value.Trim();
                    break;

                case XML.UnavailableStyle:
                    this.UnavailableStyle = (DisableStyle)Enum.Parse(typeof(DisableStyle), attr.Value);
                    break;

                case XML.NotGrantedStyle:
                    this.NotGrantedStyle = (DisableStyle)Enum.Parse(typeof(DisableStyle), attr.Value);
                    break;

                default:
                    // Ignore unsupported attributes.
                    break;
            }
        }

        private void ParseCommandChildElement(XmlElement child)
        {
            switch (child.LocalName)
            {
                case XML.ExtendedProperties:
                    ParseExtendedPropertiesElement(child);
                    break;

                default:
                    // Ignore unsupported elements.
                    break;
            }
        }

        private void ParseExtendedPropertiesElement(XmlElement xmlExtendedProperties)
        {
            EnsureCorrectElement(xmlExtendedProperties, XML.ExtendedProperties);
            foreach (XmlNode propertyOrIndexer in xmlExtendedProperties.ChildNodes)
            {
                if (propertyOrIndexer.NodeType != XmlNodeType.Element)
                    continue;

                switch (propertyOrIndexer.LocalName)
                {
                    case XML.Property:
                        ParsePropertyElement((XmlElement)propertyOrIndexer);
                        break;

                    case XML.Indexer:
                        ParseIndexerElement((XmlElement)propertyOrIndexer);
                        break;

                    default:
                        // Ignore unsupported elements.
                        break;
                }
            }
        }

        private void ParsePropertyElement(XmlElement xmlProperty)
        {
            EnsureCorrectElement(xmlProperty, XML.Property);
            string property = null;
            string value = null;
            foreach (XmlAttribute attr in xmlProperty.Attributes)
            {
                switch (attr.LocalName)
                {
                    case XML.Property:
                        property = attr.Value.Trim();
                        break;

                    case XML.Value:
                        value = attr.Value.Trim();
                        break;

                    default:
                        // Ignore unsupported attributes.
                        break;
                }
            }

            if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(value))
                throw new ArgumentException("xmlProperty", "The '" + XML.Property + "' element must contain non-empty '" + XML.Property + "' and '" + XML.Value + "' attributes.");

            this.extendedProperties.Properties.Add(property, value);
        }

        private void ParseIndexerElement(XmlElement xmlIndexer)
        {
            EnsureCorrectElement(xmlIndexer, XML.Indexer);
            string property = null;
            string index = null;
            string value = null;
            foreach (XmlAttribute attr in xmlIndexer.Attributes)
            {
                switch (attr.LocalName)
                {
                    case XML.Property:
                        property = attr.Value.Trim();
                        break;

                    case XML.Index:
                        index = attr.Value.Trim();
                        break;

                    case XML.Value:
                        value = attr.Value.Trim();
                        break;

                    default:
                        // Ignore unsupported attributes.
                        break;
                }
            }

            if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(index) || string.IsNullOrEmpty(value))
                throw new ArgumentException("xmlIndexer", "The '" + XML.Indexer + "' element must contain non-empty '" + XML.Property + "', '" + XML.Index + "' and '" + XML.Value + "' attributes.");

            this.extendedProperties.Indexers[property, index] = value;
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets or sets non-optional command key. Null is not allowed.
        /// </summary>
        /// <exception cref="ArgumentException">Generated if null reference or empty string is provided as a <b>Key</b> value.</exception>
        public string Key
        {
            get { return this.key; }
            set
            {
                if (string.IsNullOrEmpty(value))
                    throw new ArgumentException("value", "Null refence or empty string is not a valid 'Key' value.");

                this.key = value;
            }
        }

        /// <summary>
        /// Gets or sets optional text. Nullable. Empty strings are converted to <b>null</b>.
        /// </summary>
        public string Text
        {
            get { return this.text; }
            set { this.text = string.IsNullOrEmpty(value) ? null : value; }
        }

        /// <summary>
        /// Gets or sets optional description. Nullable. Empty strings are converted to <b>null</b>.
        /// </summary>
        public string Description
        {
            get { return this.description; }
            set { this.description = string.IsNullOrEmpty(value) ? null : value; }
        }

        /// <summary>
        /// Gets or sets how a command, which is not available to the current <see cref="IActor"/>, is displayed.
        /// </summary>
        /// <remarks><para>Default is <see cref="DisableStyle.Automatic"/>.</para>
        /// <para>Typically, <see cref="ICommand.IsAvailableToActor(IActor)"/> method is used to check 
        /// whether the command is available to the actor.</para></remarks>
        public DisableStyle UnavailableStyle
        {
            get { return this.unavailableStyle; }
            set { this.unavailableStyle = value; }
        }

        /// <summary>
        /// Gets or sets how a command, which the current <see cref="IActor"/> is not granted to execute, is displayed.
        /// </summary>
        /// <remarks><para>Default is <see cref="DisableStyle.Automatic"/></para>
        /// <para>Typically, <see cref="ICommand.GrantExecute(IActor)"/> is used to check whether the actor is 
        /// granted the command execution after the <see cref="ICommand.Parameters"/> have been provided.</para></remarks>
        public DisableStyle NotGrantedStyle
        {
            get { return this.notGrantedStyle; }
            set { this.notGrantedStyle = value; }
        }

        /// <summary>
        /// Gets a collection of additional properties which affect how commands are displayed in UI.
        /// </summary>
        /// <remarks>For example, ExtendedProperties may specify a CssClass property for a Web control.</remarks>
        public ObjectProperties ExtendedProperties
        {
            get { return this.extendedProperties; }
        }

        #endregion
    }
}
